package org.cogaen.net;

import java.io.IOException;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import org.cogaen.core.Core;
import org.cogaen.event.EventManager;
import org.cogaen.logging.LoggingService;

class PeerImpl implements Peer {

	private static final int PING_PKG = 1;
	
	private static final int CONNECT_PKG = 2;
	
	private static final int CONNECT_ACK_PGK = 3;
	
	private static final int DISCONNECT_PKG = 4;

	private static final int DISCONNECT_ACK_PKG = 5;
	
	private static final int CUSTOM_PGK = 6;

	/** Message time out in nano seconds. */
	private static final long MESSAGE_TIMEOUT = 3000000000L;

	/** Minimum time between messages in nano seconds. */
	private static final long PING_DELAY = 500000000L;

	/** Connection time out in nano seconds. */
	private static final long CONNECTION_TIME_OUT = 10000000000L;
	
	/** Address of this peer. */
	private SocketAddress address;
	
	/** Indicates if connection to this peer is established. */
	private boolean connected = false;
	
	/** Used to send network events. */
	private EventManager evtMngr;
	
	/** Used to log lost packages and connection loss. */
	private LoggingService logger;
	
	/** Package header used to send messages. */
	private Header header;
	
	/** Time stamp of last sent message. */
	private long lastSent = 0;
	
	/** Time stamp of last received message. */
	private long lastReceived = 0;
	
	/** Used to log sent messages. */
	private List<MessageInfo> messageLog = new ArrayList<MessageInfo>();

	/** List of messages to be sent to the assigned address. */
	private List<ByteBuffer> messagesToSend = new ArrayList<ByteBuffer>();
	
	/** List of received messages. */
	private List<ByteBuffer> receivedMessages = new ArrayList<ByteBuffer>();
	
	public PeerImpl(Core core, SocketAddress address) {
		this.evtMngr = EventManager.getInstance( core );
		this.logger = LoggingService.getInstance( core );
		this.address = address;
		this.header = new Header();
	}
	
	public void putPackage(ByteBuffer buffer) {
		// update time stamp
		lastReceived = System.nanoTime();
		
		// reader package header
		Header remoteHeader = new Header( buffer );
		
		// acknowledge packages
		this.header.acknowledge( remoteHeader.getSequenceNumber() );
		for (Iterator<MessageInfo> it = this.messageLog.iterator(); it.hasNext(); ) {
			if ( remoteHeader.isAcknowledged( it.next().sequenceNumber ) ) {
				it.remove();
			}
		}
		
		switch ( remoteHeader.getPackageId() ) {
		
		case PING_PKG:
			// intentionally left empty
			break;
			
		case CONNECT_PKG:
			this.logger.logDebug( NetworkService.LOGGING_SOURCE, "received connect message");
			send(CONNECT_ACK_PGK, null);			
			if ( !isConnected() ) {
				send(CONNECT_PKG, null);
				this.logger.logDebug( NetworkService.LOGGING_SOURCE, "sending connect acknowledge message");
			}
			break;
			
		case CONNECT_ACK_PGK:
			this.logger.logDebug( NetworkService.LOGGING_SOURCE, "received connect acknowledge message");
			this.connected = true;
			this.evtMngr.enqueueEvent( new PeerConnectedEvent( this ) );
			break;
			
		case DISCONNECT_PKG:
			this.logger.logDebug( NetworkService.LOGGING_SOURCE, "received disconnect message");
			if ( isConnected() ) {
				send(DISCONNECT_PKG, null);
				this.logger.logDebug( NetworkService.LOGGING_SOURCE, "sending disconnect acknowledge message");
			}
			send(DISCONNECT_ACK_PKG, null);
			break;
			
		case DISCONNECT_ACK_PKG:
			this.logger.logDebug( NetworkService.LOGGING_SOURCE, "received disconnect acknowledge message");
			this.connected = false;
			this.evtMngr.enqueueEvent( new PeerDisconnectedEvent( this ) );
			break;
			
		case CUSTOM_PGK:
			ByteBuffer message = ByteBuffer.allocate( buffer.remaining() );
			message.put( buffer );
			message.flip();
			this.receivedMessages.add( message );
			break;
		}
		
	}
		
	public void update(DatagramChannel channel) throws IOException {
		long time = System.nanoTime();
				
		// send queued messages
		for (ByteBuffer message : this.messagesToSend) {
			// TODO find a better way of doing this
			while ( channel.send( message, this.address ) == 0) ;
		}		
		this.messagesToSend.clear();
		
		// check for unacknowledged messages
		for (Iterator<MessageInfo> it = this.messageLog.iterator(); it.hasNext(); ) {
			MessageInfo info = it.next();
			if ( time - info.timeStamp > MESSAGE_TIMEOUT) {
				it.remove();
				this.logger.logInfo(NetworkService.LOGGING_SOURCE, "package lost (" + info.sequenceNumber + ")");
			}
		}
		
		if ( !isConnected() ) {
			return;
		}
		
		// send ping message if necessary
		if ( time - this.lastSent > PING_DELAY ) {
			send(PING_PKG, null);
		}
		
		// is remote peer still alive?
		if ( time - this.lastReceived > CONNECTION_TIME_OUT) {
			this.connected = false;
			this.messageLog.clear();
			this.evtMngr.enqueueEvent( new PeerDisconnectedEvent( this ) );
		}
	}
	
	@Override
	public void connect() {
		send(CONNECT_PKG, null);
	}

	@Override
	public void disconnect() {
		send(DISCONNECT_PKG, null);
	}

	@Override
	public boolean isConnected() {
		return this.connected;
	}

	@Override
	public boolean receive(ByteBuffer buffer) {
		if ( this.receivedMessages.isEmpty() ) {
			return false;
		}
		
		buffer.put( this.receivedMessages.remove(0) );
		return true;
	}

	@Override
	public void send(ByteBuffer buffer) {
		send(CUSTOM_PGK, buffer);
	}
	
	@Override
	public SocketAddress getAddress() {
		return this.address;
	}
	
	private void send(int id, ByteBuffer buffer) {
		this.header.incSequenceNumber();
		this.header.setPackageId( id );
		
		int size = buffer != null ? buffer.remaining() + Header.SERIALIZED_SIZE : Header.SERIALIZED_SIZE;		
		ByteBuffer message = ByteBuffer.allocate( size );
		header.serialize( message );
		if (buffer != null) {
			message.put( buffer );
		}
		message.flip();
		
		this.messagesToSend.add( message );
		this.lastSent = System.nanoTime();
		this.messageLog.add( new MessageInfo( this.header.getSequenceNumber(), this.lastSent ) );
	}
	
	private static class MessageInfo {
		
		private int sequenceNumber;
		
		private long timeStamp;
		
		public MessageInfo(int seq, long time) {
			this.sequenceNumber = seq;
			this.timeStamp = time;
		}
	}

}
